Windows Attack
Contents
Welcome to the second installment of Game Programming in Practice, a series of tutorials in which you learn about core BGT concepts by seeing how they are used. As you follow these tutorials you create actual games while the syntax, structure, and techniques of BGT programming become second nature to you.
Let me take this opportunity to remind you that errors are an integral part of programming. They happen to the best of us, and rather than seeing them as frustrating stumbling blocks, you will do much better considering them as opportunities to test and even improve your logical skills.
Finally, remember to take frequent breaks. Stepping back from a problem for a while and returning to it refreshed might make all the difference.
With these formalities stored safely in the back of your mind, let's get going!
In this tutorial you will create a game called Windows Attack. If you have ever had trouble with computer viruses in the past, you will be delighted to know that this game finally lets you take sweet revenge.
Here is the basic idea: You are an antivirus program which must target and destroy approaching computer viruses before they reach and infiltrate the system!
Let's flesh out this basic idea until it becomes a real game.
Computer viruses will fall towards your system from above, making noise as they approach. Your job is to target a virus with the left and right arrow keys until its sound is in the center of your stereo field, then hit the space bar to trigger your deadly digital destruction device of doom. If you make it in time, you will score some points depending on how well you performed. However, if a virus has made it all the way down, it will do some damage to your system. Now, the system can only stand so much damage until it crashes with the dreaded blue screen of death, at which point the game will be over.
Exercise
Based on the game's description in section 2, can you come up with a list of required sounds?
Here is the list I created:
- A start sound (start.wav)
- The sound a virus makes while falling (virus_fall.wav)
- The sound a virus makes when landing (virus_land.wav)
- The sound a virus makes when it is destroyed (virus_hit.wav)
- The sound of your deadly digital destruction device of doom (gun.wav)
- An end sound (game_over.wav)
You may save yourself most of the bother of sound design by simply using some of the standard Windows sounds, or you may record or download a unique collection of sounds for your game. In any case, I strongly recommend that you use the filenames I listed above as they will be used later on in this tutorial. It is a good idea to create a new, empty folder for your project at this point and then copy your sound files into this folder. While you are at it, now is also a good time to create the file windows_attack.bgt. This will contain the entire source code for Windows Attack and will keep us busy for the rest of this tutorial.
Every game has a game state, which is all the information required to continue the game. For example, the game state for chess would primarily consist of the positions of all the pieces on the board and also the information whose turn it is. For Uno, the game state would consist of each player's hand and the order of cards in the stock and discard piles, along with each player's current score and the information whose turn it is to discard.
Exercise
From the description of Windows Attack in section 2, identify the game state.
Here is the solution I came up with:
- Player's horizontal position on the board
- Viruses on the board, along with the current height of each virus
- Player's current score
- Player's current number of lives
Let us translate this into code:
// Game state
int player_position; // Current horizontal position of player
virus@[] board; // Locations without viruses will contain null
int lives; // Game over if this falls to zero during play
int score; // Current score
If you have completed the first tutorial in this series or read some of the BGT language tutorial, understanding the above code won't be much of a problem for you. Let us pause briefly, however, to inspect the most complicated of the lines above:
virus@[] board; // Locations without viruses will contain null
The name of the variable declared here is board, and the data type is virus@[]. This is best understood when reading it from right to left: The brackets tell us that this is an array, the @ sign indicates it is an array of handles, and the word virus specifies the type of object to which the handles will refer. The virus class will be defined later in this tutorial.
It is a useful habit to define a function which initializes the game state so that whenever we wish to start a new game, we need only call this function and can rest assured that each game begins in a well-defined initial state.
Here is that function for Windows Attack:
const int board_size = 21;
const int initial_lives = 3;
void initialize()
{
player_position = (board_size-1) / 2; // Center of board
board.resize(board_size);
for(int i=0; i<board_size; i++)
{
@board[i] = null;
}
lives = initial_lives;
score = 0;
}
You might argue that it is unnecessary to explicitly set all the board locations to null. After all, null is exactly the value with which handle variables get initialized. However, remember that we might want to play multiple games in one session, and so the board might still contain leftover viruses from the previous game. The lesson here is that a little extra housekeeping never hurts and leads to well-defined states at key points in the flow of your program.
Also, note that we have defined constants for the board size and initial number of lives. In general, it makes sense to define constants for values which will eventually be fixed but which you might change from time to time in your development process. It is of course perfectly valid to write all constants as literal numbers, but the advantage of a named constant is that you can change its value at the point of its definition and the change will apply to every point in your source code where the constant is used. As a general rule, any value which is used more than once in your source code should be given a name.
In the first part of this series of tutorials, you learned that sounds are played using sound objects, and that the number of sound objects you need is the number of sounds your game will play simultaneously. For small projects like Memory Train you will usually manage the creation, utilization and destruction of those sound objects in your own code. But when matters start to become more complex, such as when lots of sounds need to be created or repositioned in response to events in the game world, managing all the sound objects yourself can be a nuisance. For these situations, BGT contains a powerful helper class called sound_pool. It is called a pool because it can manage a large collection of sound objects and use them as needed. For example, when you order the sound pool object to play a sound, it will look through its pool of sound objects to find one which is currently inactive, and only if none can be located will a new sound object be created.
All this may sound dreadfully complicated at first, but the good news is that the sound pool class is incredibly easy to use because you do not need to concern yourself with most of its technical intricacies. You simply order it to play the right sound at the right time and place, and the sound pool will take care of the rest.
Another killer feature of the sound pool is that it can automatically position the sounds for you. You simply tell it the coordinates of your sound sources and your listener, which will usually be the player, and once again, the sound pool will take care of the rest.
Exercise
Consult the BGT reference about the sound_pool class and familiarize yourself with its methods.
One potentially confusing aspect of the sound pool is its use of so-called sound slots, so let us address them right away to prevent you from forming the wrong model about them.
When you ask the sound pool to play a sound, for example by calling the play_2d method, it will return a number called a sound slot. The sound slot is simply a number which the sound pool has assigned to the sound, in much the same way as the government assigns social security cards to citizens when they get their first jobs. The government uses social security numbers to quickly find or update a person's data. That's why they ask you for this number whenever you call them. In much the same way, when you ask the sound pool class to update a sound which is already playing, you will need to provide the sound slot which was returned to you when you initially started the sound. If some of this doesn't seem to make sense, just read on, and all will be revealed.
We already dealt with the initialization of the game state. Let us now turn to the game itself while it is in progress.
Traditionally, this is expressed as a loop which drives the action and which is therefore sometimes called a driver loop. The job of the driver loop is to run until the game is over and to orchestrate the flow of the various components of the game. In other words, the driver loop is responsible for the movement of time. Let's see what this would look like:
sound_pool pool;
void game_loop()
{
timer virus_act_timer;
timer virus_spawn_timer;
while(lives > 0) // We loop until the game is over
{
player_act(); // Check if player pressed a key and let him move or shoot
if(virus_act_timer.elapsed >= 200) // Viruses move 5 times a second
{
viruses_act();
virus_act_timer.restart();
}
if(virus_spawn_timer.elapsed >= 3000) // New virus every 3 seconds
{
virus_spawn();
virus_spawn_timer.restart();
}
wait(5); // Be nice to other apps on this machine
}
}
Our game loop references a number of functions which we still have to define. First, there is player_act, the function which allows the player to move and to shoot at viruses.
void player_act()
{
if(key_pressed(KEY_LEFT) and player_position>0)
{
player_position--;
}
else if(key_pressed(KEY_RIGHT) and player_position < (board_size-1))
{
player_position++;
}
if(key_pressed(KEY_SPACE)) // Shoot
{
shoot(); // Let's do this in its own function as it is more complex
}
if(key_pressed(KEY_ESCAPE)) // Exit game
{
lives = 0; // Simulate game over
}
pool.update_listener_2d(player_position, 0);
}
void shoot()
{
if(@board[player_position] is null) // Missed
{
pool.play_stationary("gun.wav", false); // Centered, not looping
}
else // Hit
{
board[player_position].die(); // We tell the virus it has died
score++; // Player gets a point
@board[player_position] = null; // No more virus at this position
}
}
Now, let us define the function viruses_act, which gives each virus currently on the board a chance to move.
void viruses_act()
{
for(int i=0; i<board_size; i++)
{
if(@board[i] !is null)
{
board[i].act();
}
}
}
Finally, let us define the function virus_spawn, which randomly selects a location on the board and, if the location is still vacant, places a new virus there.
void virus_spawn()
{
int location = random(0, board_size-1);
if(@board[location] is null) // Check if it is vacant
{
virus newbie(location); // Create a new virus
@board[location] = @newbie; // Register it with the game board
}
}
As if the previous two sections hadn't been code-intensive enough, let's complete the picture by defining the virus class.
class virus
{
int height; // Height above ground. When this falls to zero the virus lands
int falling_sound; // Stores the sound slot for the falling sound loop
int location; // Location of virus on the game board
virus(int location) // Constructor
{
this.location = location;
height = 20;
falling_sound = pool.play_2d("virus_fall.wav", player_position, 0, location, height, true);
}
void act()
{
height--;
if(height<=0) // Virus has landed
{
pool.destroy_sound(falling_sound);
pool.play_2d("virus_land.wav", player_position, 0, location, 0, false);
lives--;
@board[location] = null;
}
else
{
pool.update_sound_2d(falling_sound, location, height);
}
}
void die()
{
pool.destroy_sound(falling_sound);
pool.play_2d("virus_hit.wav", player_position, 0, location, height, false);
}
}
Our mission is almost complete. We have defined the game state and its initialization, and we have specified the game loop along with all its helper functions. Let us conclude by defining the main function which will set all of it in motion.
void main()
{
show_game_window("Windows Attack");
sound start_sound;
start_sound.load("start.wav");
tts_voice voice;
voice.speak("Welcome to Windows Attack!");
start_sound.play_wait();
initialize();
game_loop();
pool.destroy_all();
sound game_over_sound;
game_over_sound.load("game_over.wav");
game_over_sound.play_wait();
voice.speak_wait("Game over! Your score was " + score);
}
Now, the only thing left to do is to place the line
#include "sound_pool.bgt"
at the beginning of your source code to bring the sound_pool class into the compilation.
- Assemble the code fragments from this tutorial into a working version of Windows Attack.
- Modify your game so that instead of simply exiting when the game is over, it asks if the player would like to play again, and continues accordingly.
- Modify your game to offer multiple difficulty levels.
- Modify your game so that every minute it spawns a supervirus. A supervirus is different in that it requires three shots to take down, but if taken down it will provide the player one extra life.